Skip to content

ci(tss): gated TSS shadow publish lane v2 (TIN-3026) - #263

Closed
Jesssullivan wants to merge 5 commits into
mainfrom
jess/tin-3026-tss-shadow-publish-v2
Closed

Jesssullivan wants to merge 5 commits into
mainfrom
jess/tin-3026-tss-shadow-publish-v2

Conversation

@Jesssullivan

@Jesssullivan Jesssullivan commented Aug 28, 2026 •

Copy link
Copy Markdown
Owner

Why

tss.tinyland.dev is already publicly resolvable but still serves the old Pages build (1784027104867); it is not a current static shadow of the reviewed Tinyland blog. The site is a separately-provisioned Pages project, so changing the canonical jesssullivan.github.io deployment workflow or rebinding the public route would be unsafe. This draft adds a dedicated, opt-in dispatch path for that project only.

What changed

  • adds tss-shadow-publish-v2.yml, workflow_dispatch / typed repository_dispatch only;
  • defaults BLOG_TSS_PUBLISH_ENABLED to false and refuses to publish without it;
  • confines Cloudflare scope to a dedicated CLOUDFLARE_PAGES_TSS_PROJECT_NAME, failing closed if it is blank or equals the canonical project;
  • checks out the requested full SHA, requires it to equal current default-branch HEAD, resolves the successful build provenance with existing bounded pagination/cross-checks, and uses its exact artifact;
  • verifies project binding before upload, then fetches the public route and records the deployed source SHA.

Not in this PR (operator ceremony)

  1. Confirm the dedicated Pages project variable: current documentation identifies the expected tss-shadow project, but CLOUDFLARE_PAGES_TSS_PROJECT_NAME is not configured in repository metadata.
  2. Record the Cloudflare API token replacement/revocation evidence required by TIN-2727 before the credentialed ceremony.
  3. Set BLOG_TSS_PUBLISH_ENABLED=true, dispatch against a fresh successful canonical build, then reset it to false and record the live SHA/headers.

Merging this PR publishes nothing: BLOG_TSS_PUBLISH_ENABLED is currently absent (therefore false) and the lane has no non-dispatch trigger. After it lands, the operator must configure the dedicated TSS project variable, establish the TIN-2727 credential evidence, and deliberately enable the TSS switch for an exact-SHA dispatch. The former TIN-2801 publication hold was lifted by operator ruling on 2026-09-22; it is not a condition of this lane.

Evidence

  • Current public probe before this work: GET https://tss.tinyland.dev/blog had no tinyland-source-sha meta tag; GET /robots.txt was Allow: /; GET /_app/version.json was 1784027104867.
  • Authoritative shadow policy and route ownership: docs/blog-shadow-preview.md, docs/dns-and-cloudflare-architecture.md.
  • Existing canonical production workflow is intentionally left unchanged.

Validation

  • node --check for the workflow’s embedded scripts;
  • policy tests pass with the required tracked docs and guardrails.

@Jesssullivan

Copy link
Copy Markdown
Owner Author

Operator ceremony for the first tss publish (run in order; nothing here is performed by an agent):

  1. Rotate the Cloudflare API token (TIN-2727) in the Cloudflare dashboard — scope: Pages:Edit on the account — then store it without echoing: env -u GH_TOKEN gh secret set CLOUDFLARE_API_TOKEN --repo Jesssullivan/jesssullivan.github.io (paste at the prompt).
  2. Name the Pages project that owns tss.tinyland.dev (Cloudflare → Pages → the project whose custom domain is tss.tinyland.dev): env -u GH_TOKEN gh variable set CLOUDFLARE_PAGES_TSS_PROJECT_NAME --repo Jesssullivan/jesssullivan.github.io --body '<project-name>' (if the custom domain is bound to a non-main branch, also set CLOUDFLARE_PAGES_TSS_BRANCH).
  3. Enable the switch: env -u GH_TOKEN gh variable set BLOG_TSS_PUBLISH_ENABLED --repo Jesssullivan/jesssullivan.github.io --body true.
  4. Dispatch at current main (after this PR merges): SHA=$(git rev-parse origin/main) && env -u GH_TOKEN gh api repos/Jesssullivan/jesssullivan.github.io/dispatches -f event_type=tss-shadow-publish-v2 -f "client_payload[source_sha]=$SHA" -f 'client_payload[deploy]=true'. For an open PR head add -f 'client_payload[source_pr]=<n>'.
  5. Verify: curl -s https://tss.tinyland.dev/_app/version.json (must exceed 1784027104867) and curl -s https://tss.tinyland.dev/blog | grep -o '<meta name="tinyland-source-sha"[^>]*>' (must equal $SHA); every route must carry noindex,nofollow.

Merging this PR alone changes nothing on any served surface.

@Jesssullivan

Copy link
Copy Markdown
Owner Author

Reworked at HEAD after the adversarial review of the first cut (blocker confirmed: the publish job executed PR-head code in the same job that held the Cloudflare token).

  • Split into build (contents: read, no secret, no id-token) → deploy (never checks out or runs source-tree code; downloads the artifact, re-derives and asserts the digest, revalidates switch + source, then wrangler pages deploy) — the same shape as shadow-source-build-v2 → shadow-source-publish-v2. The authority test now pins the split: the build job cannot contain secrets./wrangler/id-token, the deploy job cannot contain actions/checkout/npm ci/npm run/npx tsx.
  • Project name: validated as a lowercase slug in the resolver, refused when it equals the production project even with surrounding whitespace, passed quoted; defaults to tss-shadow (recorded three times in docs/dns-architecture.md — my PR body was wrong that no repo records it).
  • Trigger allowlist: the on: block must be exactly the typed repository_dispatch; workflow_id: "ci.yml" and head_sha filtering pinned.
  • Shadow crawl posture beyond the meta: robots.txt Disallow: / + _headers X-Robots-Tag: noindex, nofollow written into the artifact and asserted in the deploy job.
  • Post-publish proof: the run fails unless tss.tinyland.dev/blog serves the published tinyland-source-sha within four minutes.
  • AGENTS.md sentence fixed (two credentialed Cloudflare lanes, not one).

Open from the review, deliberately not changed here: the pre-publish getRepoVariable recheck is the pattern all four sibling v2 lanes use and none has ever executed — the deploy job carries actions: read, which is the permission the variables read endpoint requires, but the first real dispatch is the proof. If it 403s, the lane fails closed (rethrown), not open.

Local: node scripts/test-workflow-authority.mjs → all 6 fixture suites green (file reads only, no build). Hosted: pending.

@Jesssullivan

Copy link
Copy Markdown
Owner Author

Three things before this can be dispatched (the job split and served-SHA
proof on 53db1c9 look right):

  1. .github/workflows/tss-shadow-publish-v2.yml:207-229 cannot succeed.
    GITHUB_TOKEN has no Variables permission, so getRepoVariable returns
    403 and :225 rethrows. The lane will fail after a ~10 min build, every
    time. No sibling lane has ever executed this call. Replace with a job
    boundary: if: vars.BLOG_TSS_PUBLISH_ENABLED == 'true' on the deploy
    job, keep the getRef/pulls.get freshness checks as a script step.
  2. :199-203 compares exactly while :251 interpolates. transscendsurvival-org
    (trailing space) passes the guard, then wrangler-action's argStringToArray
    drops the empty token and deploys production. Validate against the Pages
    name grammar and compare a normalized value.
  3. scripts/test-workflow-authority.mjs:177-198: add workflow_id: "ci.yml"
    (pinned for the production and rollback lanes at :128/:253, missing here)
    and assert the PR-checkout job contains no secrets..

@Jesssullivan

Copy link
Copy Markdown
Owner Author

Fixed at HEAD: (1) the kill switch is now re-read by Actions at deploy-job start (if: needs.build.result == 'success' && vars.BLOG_TSS_PUBLISH_ENABLED == 'true') and asserted again inside the pre-publish step from vars, with the main-head / PR-open freshness checks kept; getRepoVariable is gone and the authority contract forbids it in this lane. (2) was already addressed at 53db1c9 (slug regex + trim in the resolver, quoted interpolation). (3) workflow_id: "ci.yml" and the no-secrets.-in-the-checkout-job assertion were already pinned at 53db1c9. The same unexecutable getRepoVariable recheck exists in the four sibling v2 lanes — separate ticket, not this PR.

tss.tinyland.dev has had no publisher since the legacy shadow workflow was
retired on 2026-08-13; it still serves the 2026-07-14 build. This adds the
default-branch-owned, dispatch-only lane TIN-3026 names as the separately
authorized apply path for the public noindex development shadow:

- resolves an exact current-main SHA or an open same-repo PR head, requires a
  successful canonical CI run at that SHA with build-and-test and
  bazel-remote-gates green, and fails closed unless deploy=true and
  BLOG_TSS_PUBLISH_ENABLED=true;
- builds with PUBLIC_DEPLOY_TIER=shadow (site-wide noindex + source-sha meta,
  validated by validate-deploy-tier-output.mjs), records the static digest,
  revalidates the kill switch and the source immediately before publish, and
  refuses the production Pages project by name;
- publishes to CLOUDFLARE_PAGES_TSS_PROJECT_NAME only (repo var, required).

Resolver fixtures cover every refusal path; the workflow-authority contract
pins the lane shape and forbids production reach or manual carriers.
AGENTS.md registers the lane; docs/tss-shadow-publish.md records the ceremony.
…ified deploy

Review finding on the first cut: the publish job ran npm ci / npm run build
on the requested PR head in the same job that held the Cloudflare account
token, so source-tree code could reach the token and the production-project
name guard was decorative. The lane now mirrors shadow-source-build-v2 →
shadow-source-publish-v2: the build job has contents:read only and no
secret, uploads the built tree with its digest; the deploy job never checks
out or executes source-tree code, re-derives the digest from the downloaded
artifact, revalidates the kill switch and the source, and only then runs
wrangler against the artifact.

Also: the Pages project name is validated as a slug and refused when it is
the production project (trailing whitespace can no longer slip past an exact
compare into a shell word), the project defaults to tss-shadow as recorded in
docs/dns-architecture.md, the canonical-CI lookup filters by head_sha, the
shadow ships robots.txt Disallow plus an X-Robots-Tag header alongside the
HTML meta, the run fails unless tss.tinyland.dev serves the published
source SHA within four minutes, the authority test pins the single trigger,
the build/deploy split, workflow_id ci.yml and the step order, and AGENTS.md
no longer says Cloudflare publication comes only from the production lane.
…iables API

Review finding: GITHUB_TOKEN carries no Variables permission, so the
getRepoVariable recheck copied from the sibling v2 lanes would 403 and
refuse every publish after a full build. The deploy job is now gated with
'if: vars.BLOG_TSS_PUBLISH_ENABLED == true', which Actions resolves when
the job is scheduled (after the build), and the pre-publish step keeps the
source freshness checks (main head / PR still open at the exact SHA). The
authority contract now forbids getRepoVariable in this lane.
@Jesssullivan
Jesssullivan force-pushed the jess/tin-3026-tss-shadow-publish-v2 branch from 2b6f2bd to 8929f27 Compare August 28, 2026 20:17
@Jesssullivan
Jesssullivan force-pushed the jess/tin-3026-tss-shadow-publish-v2 branch from 8929f27 to 55f3865 Compare August 28, 2026 20:17
@Jesssullivan

Copy link
Copy Markdown
Owner Author

Closed as superseded by #281 (exact-source TSS Pages direct publisher, head d756ee2), per operator ruling TIN-3692 RM12 (comment bb016df6) and addendum section 4 ("blog: close #263 as superseded by #281"). Head 55f3865 stays reachable on branch jess/tin-3026-tss-shadow-publish-v2; the branch is not deleted. TIN-3026 / TIN-2727. Authority: R-HOOK-CONVERGENCE-20261004 / R-N11 / R-N13.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant